<?php
/*
 * Copyright (c) 2008 Sergey Bondari, bondari@bondari.net
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * - The name of the author may not be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

namespace Mosaic\System;


/**
 * Inter component communication facility
 *
 * Inter Component Communication facility supports 3 types of inter 
 * communication modes: synchronous event, asynchronous event and tile processing. 
 * Each one of these functionalities is invoked by calling a corresponding ICC method.
 *
 * @package System
 * @subpackage Node
 */
class ICC {

	/**
	 * Trigger synchronous event notification
	 *
	 * First starts event locally, then sends event to remote components synchronously one by one
	 *
	 * @param ifEvent Event object instance
	 * @param boolean If true, run event only on current node.
	 * @return void
	 */
	public function fireEventSync(ifEvent $event, $onlyLocal = false) {
		global $sys;

		trigger_error('+Firing synchrnonous event '.$event->getName());

		try {

			/*
			 * Get component instances
			 */
			$components = $sys->componentBroker->getProvidingComponentInstances('mosaic:resource:event', $event->getName());


			/*
			 * Run events locally first
			 */
			foreach ($components as $component) {
				if ($component instanceof ifRemoteComponent) continue;

				$component->onEvent($event);

				if ($event->isCancelled()) {
					trigger_error('Component '.$component->componentInfo()->systemID.' has stopped event execution');
					break;
				}
			}


			/*
			 * Run events remotely
			 */
			if (!$onlyLocal && !$event->isCancelled()) {
				foreach ($components as $component) {
					if ($component instanceof ifRemoteComponent) {
						trigger_error('Sending event to '.$component->getNodeName());

						/*
						 * Encapsulate event
						 */
						$req = new ResourceRequest($event);

						/*
						 * Send and receive event
						 */
						$event->merge(Resource::deserialize($req->execSync($component->getNodeName())));

						if ($event->isCancelled()) {
							trigger_error('Component '.$component->componentInfo()->systemID.' has stopped event execution');
							break;
						}

					}
				}
			}


		} catch (KernelException $e) {
			trigger_error('Unhandled event '.$event->getName().', '.$e->getMessage());
		}

		trigger_error('-Event '.$event->getName().' execution finished.');

	}

	
	/**
	 * Trigger asynchronous event notification
	 *
	 * If only local components are serving the event, event is running in synchronous mode,
	 * otherwise ICC enqueues all async requests to multiplexor and adds a callback to run event
	 * locally when last response arrives.
	 *
	 * @param ifEvent Event object instance
	 * @return void
	 */
	public function fireEventAsync(ifEvent $event) {
		global $sys;
		trigger_error('+Fire asynchronous event'.$event->getName());

		try {
			/*
			 * Get component instances
			 */
			$components = $sys->componentBroker->getProvidingComponentInstances('mosaic:resource:event', $event->getName());


			/*
			 * Prepare information what components will be involved: remote and/or local
			 */
			$remoteComponentsCnt = 0;
			$localComponentsCnt = 0;

			foreach ($components as $component) {
				if ($component instanceof ifRemoteComponent) {
					$remoteComponentsCnt++;
				} else {
					$localComponentsCnt++;
				}
			}

			if ($remoteComponentsCnt) { // there are remote components
				/*
				 * Process event locally after last response arrives
				 */
				if ($localComponentsCnt) {
					$event->pushCallback(function() use ($event) { $sys->icc->fireEventSync($event, true); });
				}

				foreach ($components as $component) {
					if ($component instanceof ifRemoteComponent) {
						/*
						 * Add lambda function for each remote event except last. 
						 * Last would trigger local event processing.
						 */
						if (--$remoteComponentsCnt) $event->pushCallback(function() { });


						/*
						 * Send event to remote node
						 */
						trigger_error('Sending event to '.$component->getNodeName());
						$req = new ResourceRequest($event);
						$req->execAsync(function ($res) use ($event) { 
									$rEvent = Resource::deserialize($res->getData()); 
									$event->merge($rEvent); 
									if ($event->hasCallback()) $event->popCallback(); 
							}, $component->getNodeName());
					}
				}
			} else if ($localComponentsCnt) { // there are only local components. running event synchronously.
				$this->fireEventSync($event, true);
			}



		} catch (KernelException $e) {
			trigger_error('There is no component to process \''.$event->getName().'\'');
			trigger_error('Kernel reports '.$e->getMessage());
			if ($event->hasCallback()) $event->popCallback();

		} catch (\Exception $e) {
			$ret = '<error>Event '.$event->getName().' processing failed.</error>';
			trigger_error('Event handler has failed with message: '.$e->getMessage());
			if ($event->hasCallback()) $event->popCallback();
		}
	}

	/**
	 * Process tile resource
	 *
	 * If only local components are serving the tile, tile is processed in synchronous mode,
	 * otherwise ICC enqueues all async requests to multiplexor and adds a callback to process tile
	 * locally when last remote tile is processed and response arrives. Very similar to asynchronous
	 * event processing.
	 *
	 * @param ifTile Tile resource object instance
	 * @param boolean If true, process tile only on current node.
	 * @return void
	 */
	public function parseTile(ifTile $tile, $onlyLocal = false) {
		global $sys;
		trigger_error('+Parsing a tile '.$tile->getTagName());

		try {
			/*
			 * Get component instances
			 */
			$components = $sys->componentBroker->getProvidingComponentInstances('mosaic:resource:tile', $tile->getTagName());


			if (!$onlyLocal) { // run all

				/*
				 * Prepare information what components will be involved: remote and/or local
				 */
				$remoteComponentsCnt = 0;
				$localComponentsCnt = 0;

				foreach ($components as $component) {
					if ($component instanceof ifRemoteComponent) {
						$remoteComponentsCnt++;
					} else {
						$localComponentsCnt++;
					}
				}

				if ($remoteComponentsCnt) { // there are remote components
					/*
					 * Process tiles locally after last response arribes
					 */
					if ($localComponentsCnt) {
						$tile->pushCallback(function() use ($tile) { $sys->icc->parseTile($tile, true); });
					}

					foreach ($components as $component) {
						if ($component instanceof ifRemoteComponent) {
							/*
							 * Add lambda function for each remote tile except last. 
							 * Last would trigger local tile processing and parser content ready.
							 */
							if (--$remoteComponentsCnt) $tile->pushCallback(function() { });


							/*
							 * Send tile
							 */
							trigger_error('Sending tile to '.$component->getNodeName());
							$req = new ResourceRequest($tile);
							$req->execAsync(function ($res) use ($tile) { 
										$rTile = Resource::deserialize($res->getData()); 
										$tile->merge($rTile); 
										$tile->popCallback(); 
								}, $component->getNodeName());
						}
					}	
				} else if ($localComponentsCnt) { // there are local components
					$ret = '';
					foreach ($components as $component) {
						ob_start();
						$component->onTileParse($tile);
						$ret .= ob_get_contents();
						ob_end_clean();
					}
					$tile->setParsedXML($ret);
					$tile->popCallback();

				}



			} else { // run only local

				$ret = '';
				foreach ($components as $component) {
					if ($component instanceof ifRemoteComponent) continue;
					ob_start();
					$component->onTileParse($tile);
					$ret .= ob_get_contents();
					ob_end_clean();
				}

				$tile->setParsedXML($ret);
				$tile->popCallback();

			}
		} catch (KernelException $e) {
			$ret = '<error>Tile '.$tile->getTagName().' cannot be processed.</error>';
			trigger_error('There is no tile parser for tile \''.$tile->getTagName().'\'');
			trigger_error('Kernel reports '.$e->getMessage());
			$tile->setParsedXML($ret);
			$tile->popCallback();

		} catch (\Exception $e) {
			$ret = '<error>Tile '.$tile->getTagName().' processing failed.</error>';
			trigger_error('Tile processor has failed with message: '.$e->getMessage());
			ob_end_clean();
			$tile->setParsedXML($ret);
			$tile->popCallback();

		}

	}



	/**
	 * Process incoming resource
	 *
	 * This function is called upon incoming resource processing request
	 *
	 * @param Resource
	 * @return void
	 */
	public function processResource(Resource $res) {
		if ($res instanceof ifTile) {
			$this->parseTile($res, true);
		}

		if ($res instanceof ifEvent) {
			$this->fireEventSync($res, true);
			if ($res->hasCallback()) $res->popCallback();
		}
	}
}
